﻿using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using Skugo.Shared;

namespace Skugo.Core
{
    // The connection state is used for communicating between threads
    public enum ConnectionState
    {
        Disconnected,
        ConnectionFailed,
        Connected,
    }


    // A delegate the describes any kind of connection event
    public delegate void ConnectionFn(Client client);

    // A delegate that receives data
    public delegate void ReceiveFn(Client client, Byte[] data);

	// The client is our main route for communicating with the host
    public class Client : IWriter, IDisposable
    {
		// An event that occurs when the client connects
		public event ConnectionFn OnConnected;

		// An event that occurs when the client fails to connect
		public event ConnectionFn OnConnectionFailed;

		// An event that occurs when the client disconnects
		public event ConnectionFn OnDisconnected;

		// An event that occurs when the client receives data
        public event ReceiveFn OnReceivedData;

        /// <summary>
        /// The current state of the connection.
        /// </summary>
        public ConnectionState State { get; private set; }


		// The actual client we'll use to manage the connection
		private Socket Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

		// Store all the connection changes (in a thread safe queue)
		private ConcurrentQueue<ConnectionState> ConnectionChanges = new ConcurrentQueue<ConnectionState>();

		// All incoming data is routed through here (in a thread safe queue)
		private ConcurrentQueue<Byte[]> Incoming = new ConcurrentQueue<Byte[]>();

		// The receive buffer we use to receive data
		// This buffer is only allocated once, but is used on a different thread
		// other than the one the client was created on
		private Byte[] ReceiveBuffer;

		// Store information about data we're trying to send
		private class SendData
		{
			public Byte[] Data;
			public Int32 AmountSent = 0;
		}

        public virtual void Dispose()
        {
            // Disconnect all events
            this.OnConnected = null;
            this.OnConnectionFailed = null;
            this.OnDisconnected = null;
            this.OnReceivedData = null;

            // Change our state
            this.State = ConnectionState.Disconnected;

            try
            {
                // Close the socket
                this.Socket.Disconnect(false);
            }
            catch (SocketException)
            {
            }
        }

		// Responsible for sending out client events
		public void Update()
		{
			// Store the connection state
			ConnectionState result;

			// Try to read the connection state from the queue
            while (this.ConnectionChanges.TryDequeue(out result))
			{
                // Set the new connection state
                this.State = result;

				// If we connected...
				if (result == ConnectionState.Connected)
				{
					// Check if we have any listeners
                    if (this.OnConnected != null)
					{
						// Invoke the connection event
                        this.OnConnected.Invoke(this);
					}
				}
				// If we failed to connect...
				else if (result == ConnectionState.ConnectionFailed)
				{
					// Check if we have any listeners
                    if (this.OnConnectionFailed != null)
					{
						// Invoke the connection failed event
                        this.OnConnectionFailed.Invoke(this);
					}
				}
				// Otherwise, we must have disconnected...
				else if (result == ConnectionState.Disconnected)
				{
					// Check if we have any listeners
                    if (this.OnDisconnected != null)
					{
						// Invoke the disconnection event
                        this.OnDisconnected.Invoke(this);
					}
				}
			}

			// Store a byte array to read data into
            Byte[] readData = null;

            // Create a list to store all the data we pull from the socket thread
            List<Byte[]> dequeuedData = new List<Byte[]>();

            // If we have pending data, and if we have listeners...
            while (this.Incoming.TryDequeue(out readData))
            {
                // Add all the data to the list
                dequeuedData.Add(readData);
            }

            // Loop through all the data we received
            foreach (var data in dequeuedData)
            {
                // If we have listeners for pending data...
                // We have to check each time because technically a plugin could be created in between
                if (this.OnReceivedData != null)
                { 
                    // Tell any listeners that the client has received data
                    this.OnReceivedData.Invoke(this, data);
                }
            }
		}

		// Connect to a given host on a given port
        public void Connect(String host, UInt16 port)
        {
			// Lanch an asynchronous connection
            this.Socket.BeginConnect(host, port, ConnectionResult, null);

            // Attempt to set the receive buffer size
            this.Socket.ReceiveBufferSize = 7680000;

			// Create the receive buffer based on the max data we can receive
            this.ReceiveBuffer = new Byte[this.Socket.ReceiveBufferSize];
        }

		// Occurs when a connection has either failed or completed
        private void ConnectionResult(IAsyncResult ar)
        {
			try
			{
				// End the asynchronous call
                this.Socket.EndConnect(ar);

				// Now that we're connected, begin receiving
                this.Socket.BeginReceive(this.ReceiveBuffer, 0, this.ReceiveBuffer.Length, SocketFlags.None, ReceiveResult, null);

                // If we got here, then we must have connected!
                this.ConnectionChanges.Enqueue(ConnectionState.Connected);
			}
			catch (SocketException)
			{
                // We failed to connect for some reason :(
                this.ForceShutdown();
                this.ConnectionChanges.Enqueue(ConnectionState.ConnectionFailed);
			}
        }
		
		// Send data to the other side
        public void Write(Byte[] data)
        {
			// Only send data if we're connected...
            if (this.Socket.Connected)
            {
                try
                {
                    // Asynchronously send the data
                    this.Socket.BeginSend(data, 0, data.Length, SocketFlags.None, SendResult, new SendData() { Data = data });
                }
                catch (SocketException)
                {
                    // A socket error occurred, so we disconnected
                    this.ForceShutdown();
                    this.ConnectionChanges.Enqueue(ConnectionState.Disconnected);
                }
			}
        }

		// Occurs when part (or all) of the data finishes sending
        private void SendResult(IAsyncResult ar)
        {
            try
            {
                // Get the send data from the state
                SendData sendData = (SendData)ar.AsyncState;

                // Get the number of bytes sent
                Int32 bytesSent = this.Socket.EndSend(ar);

                // Track how many bytes were sent out of the whole data packet
                sendData.AmountSent += bytesSent;

                // If we haven't sent the entire packet...
                if (sendData.AmountSent < sendData.Data.Length)
                {
                    // Compute the amount of data we have left to send
                    Int32 dataLeft = sendData.Data.Length - sendData.AmountSent;

                    // Attempt to send the data again asynchronously
                    this.Socket.BeginSend(sendData.Data, sendData.AmountSent, dataLeft, SocketFlags.None, SendResult, sendData);
                }
            }
            catch (SocketException)
            {
                // A socket error occurred, so we disconnected
                this.ForceShutdown();
                this.ConnectionChanges.Enqueue(ConnectionState.Disconnected);
            }
        }

		// Occurs when we receive data from the socket
        private void ReceiveResult(IAsyncResult ar)
        {
            try
            {
                // Get the number of bytes received
                Int32 bytesReceived = this.Socket.EndReceive(ar);

                // Create a new buffer to hold all the received data
                Byte[] newBuffer = new Byte[bytesReceived];

                // Copy the received data into the new buffer
                Buffer.BlockCopy(this.ReceiveBuffer, 0, newBuffer, 0, bytesReceived);

                // Add the new buffer to the thread safe queue of incoming data
                // (Right now, we're not on the same thread as the object was created)
                this.Incoming.Enqueue(newBuffer);

                // Start receiving data again
                this.Socket.BeginReceive(this.ReceiveBuffer, 0, this.ReceiveBuffer.Length, SocketFlags.None, ReceiveResult, null);
            }
            catch (SocketException)
            {
                // A socket error occurred, so we disconnected
                this.ForceShutdown();
                this.ConnectionChanges.Enqueue(ConnectionState.Disconnected);
            }
        }

        private void ForceShutdown()
        {
            // Disconnect the socket and make it available for reuse
            try { this.Socket.Shutdown(SocketShutdown.Both); } catch { }
            try { this.Socket.Disconnect(true); } catch { }
        }
    }
}
